<?php
/**
 * Eka Framework 0.3.0
 *
 * @copyright Copyright (c) 2008 Alexey Yu. Chuprina <656b61@gmail.com> (http://php-eka.googlecode.com)
 * @license http://www.opensource.org/licenses/mit-license.php MIT License
 *
 * @version $Id: Debugger.php 43 2009-05-23 11:48:23Z 656b61 $
 */

/**
 * Eka_Debugger
 *
 * @package Debugger
 */
class Eka_Debugger
{
	/**
	 * @var Eka_Debugger
	 */
	static protected $instance;

	/**
	 * Singleton instance
	 *
	 * @return Eka_Debugger
	 */
	static protected function getInstance($config = null)
	{
		if (!self::$instance) {
			self::$instance = new self($config);
		}
		return self::$instance;
	}

	/**
	 * Turn on debugger
	 */
	static public function run($config = null)
	{
		self::getInstance($config);
	}
	
	/**
	 * @return bool
	 */
	static public function isRun()
	{
		return null !== $this->instance;
	}

	/**
	 * Trace arguments
	 */
	static public function trace()
	{
		$args = func_get_args();
		self::getInstance()->doTrace($args);
	}

	/**
	 * Trace arguments and die
	 */
	static public function traced()
	{
		$args = func_get_args();
		self::getInstance()->doTraced($args);
	}

	/**
	 * Log arguments
	 */
	static public function log()
	{
		$args = func_get_args();
		self::getInstance()->doLog($args);
	}
	
	static public function logException(Exception $e)
	{
		self::getInstance()->doLogException($e);
	}
	
	/**
	 * Initialize global debug function
	 *
	 * trace(), traced()
	 *
	 * @return unknown_type
	 */
	static public function initFunctions()
	{
		require_once 'Eka/Debugger/Functions.php';
	}

	############################################################################

	/**
	 * @var bool
	 */
	protected $show = true;

	/**
	 * @var bool
	 */
	protected $write = false;

	/**
	 * @var array
	 */
	protected $errorTypes = array(
		E_NOTICE => 'Notice',
		E_WARNING => 'Warning',
		E_RECOVERABLE_ERROR => 'Recoverable Error',
		E_STRICT => 'Strict',
		E_USER_ERROR => 'User Error',
		E_USER_NOTICE => 'User Notice',
		E_USER_WARNING => 'User Warning'
		);

	/**
	 * Path to logs directory
	 *
	 * @var string
	 */
	protected $logPath;

	/**
	 * Log file name for php errors
	 *
	 * @var string
	 */
	protected $errorFile = 'debug-errors.log';

	/**
	 * Log file name for exceptions
	 *
	 * @var string
	 */
	protected $exceptionFile = 'debug-exception.log';

	/**
	 * Log file name for user logs
	 *
	 * @var string
	 */
	protected $logFile = 'log.log';

	/**
	 * @var resource
	 */
	protected $logFileResource;

	/**
	 * Error info output format
	 *
	 * @var string
	 */
	protected $errorMessageFormat = "PHP Error: %type% (%code%)\nFile: %file% [%line%]\nMessage: %message%";

	/**
	 * Error info log format
	 *
	 * @var string
	 */
	//protected $errorLogFormat = "> %date%\t%type% (%code%)\t%file% [%line%]\n%message%\n---\n%context%\n";
	protected $errorLogFormat = "> %date%\t%type% (%code%)\t%file% [%line%]\t%message%\n";

	/**
	 * Exception info output format
	 *
	 * @var string
	 */
	protected $exceptionMessageFormat = "Exception: %name% (%code%)\nMessage: %message%\nFile: %file% [%line%]\nTrace:\n%trace%";

	/**
	 * Exception info log format
	 *
	 * @var string
	 */
	protected $exceptionLogFormat = "> %date%\t%name% (%code%)\t%file% [%line%]\t%message%\n%trace%\n";

	/**
	 * Message
	 *
	 * @var string
	 */
	protected $message = null;

	/**
	 * Output charset
	 *
	 * @var string
	 */
	protected $charset = 'utf-8';

	/**
	 * Content type
	 *
	 * @var string
	 */
	protected $contentType = 'text/plain';
	
	/**
	 * @var string
	 */
	protected $traceBuffer = '';

	/**
	 * Constructor
	 */
	protected function __construct($config = null)
	{
		ob_start();
		ini_set('display_errors', true);
		ini_set('html_errors', false);
		error_reporting(E_ALL | E_STRICT);

		date_default_timezone_set('Europe/Moscow');
		
		set_error_handler(array($this, 'errorHandler'));
		set_exception_handler(array($this, 'exceptionHandler'));

		if ($config) {
			if (isset($config['write'])) $this->write = (bool)$config['write'];
			if (isset($config['show'])) $this->show = (bool)$config['show'];
			if (isset($config['charset'])) $this->charset = $config['charset'];
			if (isset($config['contentType'])) $this->contentType = $config['contentType'];
			if (isset($config['message'])) $this->message = $config['message'];
			if (isset($config['path'])) $this->logPath = realpath($config['path']);
		}
		
		if ($this->write && !$this->logPath) {
			include_once 'Eka/Debugger/Exception.php';
			throw new Eka_Debugger_Exception('Path to logs directory not set');
		}
	}

	/**
	 * Error handler
	 *
	 * @param int $code
	 * @param string $message
	 * @param string $file
	 * @param int $line
	 * @param mixed $context
	 */
	public function errorHandler($code, $message, $file, $line, $context)
	{
		if ((error_reporting() & $code) == 0) {
			return;
		}

		if ($this->write || $this->show) {
			$replace = array(
				'%date%' => self::getCurrentDate(),
				'%code%' => $code,
				'%type%' => $this->errorTypes[$code],
				'%message%' => $message,
				'%file%' => $file,
				'%line%' => $line,
				'%context%' => print_r($context, true)
				);
		}
		if ($this->write) {
			$f = fopen($this->logPath . '/' . $this->errorFile, 'a');
			fwrite($f, strtr($this->errorLogFormat, $replace));
			fclose($f);
		}

		self::doDie($this->show ? strtr($this->errorMessageFormat, $replace) : null);
	}

	/**
	 * Exception handler
	 *
	 * @param Exception $e
	 */
	public function exceptionHandler(Exception $e)
	{
		$replace = $this->doLogException($e);

		self::doDie($this->show ? strtr($this->exceptionMessageFormat, $replace) : null);
	}

	protected function doLogException(Exception $e)
	{
		$replace = array(
			'%date%' => self::getCurrentDate(),
			'%name%' => get_class($e),
			'%code%' => $e->getCode(),
			'%message%' => $e->getMessage(),
			'%file%' => $e->getFile(),
			'%line%' => $e->getLine(),
			'%trace%' => $e->getTraceAsString(),
			'%fullTrace%' => print_r($e->getTrace(), true)
			);
			
		if ($this->write) {
			$f = fopen($this->logPath . '/' . $this->exceptionFile, 'a');
			fwrite($f, strtr($this->exceptionLogFormat, $replace));
			fclose($f);
		}
		
		return $replace;
	}
	
	/**
	 * @param array $args
	 * @return string
	 */
	protected function serialize($args)
	{
		$result = '';
		foreach ($args as $var) {
			if (is_bool($var)) {
				$result .= $var ? '[TRUE]' : '[FALSE]';
			} else if (is_null($var)) {
				$result .= '[NULL]';
			} else if (is_string($var) && $var == '') {
				$result .= '[empty string]';
			} else {
				$result .= print_r($var, true);
			}
			if (count($args) > 0) {
				$result .= "\t";
			}
		}
		$result .= "\n";
		return $result;
	}

	/**
	 * Clean output buffer
	 */
	protected function cleanOutputBuffer()
	{
		while (ob_get_level() > 0) {
			ob_end_clean();
		}
	}

	/**
	 * Send 500 Internal Server Error
	 *
	 * @var string $message
	 */
	protected function doDie($message = null)
	{
		$this->cleanOutputBuffer();

		if (null === $message) {
			$message = $this->message;
		}

		header('HTTP/1.1 500');
		header('Content-Type: ' . $this->contentType . '; charset=' . $this->charset);
		echo $message;
		exit;
	}

	/**
	 * Return formated current date
	 *
	 * @return string
	 */
	protected function getCurrentDate()
	{
		return date('Y-m-d H:i:s') . strrchr(microtime(true), '.');
	}

	/**
	 * Trace arguments
	 */
	protected function doTrace($args)
	{
		$text = $this->serialize($args);
		$this->traceBuffer .= $text;
		echo $text;
	}

	/**
	 * Trace arguments and die
	 */
	protected function doTraced($args)
	{
		$this->cleanOutputBuffer();
		header('Content-Type: text/plain; charset=' . $this->charset);
		if ($this->traceBuffer !== '') {
			echo $this->traceBuffer;
		}
		echo $this->serialize($args);
		exit;
	}

	/**
	 * Log arguments
	 */
	protected function doLog($args)
	{
		if (!$this->logPath) {
			include_once 'Eka/Debugger/Exception.php';
			throw new Eka_Debugger_Exception('Path to logs directory not set');
		}

		$string = "> " . $this->getCurrentDate() . "\n" . $this->serialize($args);

		if (!is_resource($this->logFileResource)) {
			$this->logFileResource = fopen($this->logPath . '/' . $this->logFile, 'a');
		}

		fwrite($this->logFileResource, $string);
	}

	/**
	 * Force singleton
	 */
	protected function __clone()
	{}
}